package org.cyberz.jsm;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import org.apache.log4j.Logger;

/**
 * The FSM proxy handler.
 * 
 * FIXME: should implement a separate compilation phase and invoke.
 * 
 * @author Andrea Leofreddi
 */
class FsmProxyHandler implements InvocationHandler {
	private final static Logger logger = Logger.getLogger(FsmBuilder.class);

	private FsmHandler handler;

	private Object inputClass;

	public synchronized Object invoke(Object proxy, Method method, Object[] methodArgs) throws Throwable {
		String inputName = method.getName();

		// Java legacy method toString
		if(inputName.equals("toString"))
			return "proxy";

		// Java legacy method equals
		if(inputName.equals("equals"))
			return false;

		// Detect reentrant input requests and queue them
		synchronized(handler) {
			if(handler.reentrantInput) {
				if(logger.isDebugEnabled())
					logger.debug("Reentrant input " + inputName + " in current state " + inputName + " (process postponed)");

				// Queue request
				handler.reentrantInputs.add(new Pair<Method, Object[]>(method, methodArgs));

				return null;
			} else
				handler.reentrantInput = true;
		}

		if(logger.isDebugEnabled())
			logger.debug("Process input " + inputName + " in current state " + handler.getCurrentState());

		// Produce args array (with proxy as first argument) and methodArgs
		Object[] args = new Object[methodArgs != null ? methodArgs.length + 1 : 1];

		args[0] = proxy;

		for(int i = 1; i < args.length; i++)
			args[i] = methodArgs[i - 1];

		// Produce parameter types
		Class<?>[] parameterTypes = new Class[args.length];

		parameterTypes[0] = (Class<?>)inputClass;

		for(int i = 1; i < args.length; i++)
			parameterTypes[i] = args[i].getClass();      

		// Look in transition table for proper transitions
		FsmTransitionTable d = handler.getClass().getAnnotation(FsmTransitionTable.class);

		if(d == null)
			throw new FsmCompileException("Handler missing the FsmTransitionTable annotation");

		FsmTransition t = null;

		for(FsmTransition u : d.value()) {
			// Check current state
			if(!u.from().equals(handler.getCurrentState()) && !u.from().equals("*"))
				continue;

			// Check input name
			if(!u.on().equals(inputName) && !u.on().equals("*"))
				continue;

			// Check priority
			if(t == null || u.priority() > t.priority())
				t = u;
		}

		// Check if a proper transition has been found
		if(t == null) {
			logger.warn("No match for " + method.getName() + " on " + handler.getCurrentState());

			throw new UnhandledInput(method.getName(), parameterTypes);
		}

		// Set current state
		logger.info("Transitioning from " + handler.getCurrentState() + " to " + t.to() + " with input " + inputName);

		handleLeavingState(proxy);

		handler.setCurrentState(t.to());

		handleEnteringState(proxy);

		// Clear reentrant input flag and handle inputs queue (if any)
		synchronized(handler) {
			if(handler.reentrantInput) {
				handler.reentrantInput = false;

				Pair<Method, Object[]> r = handler.reentrantInputs.poll();

				// Note: invoke will handle the request recursively
				if(r != null)
					r.getFirst().invoke(proxy, r.getSecond());
			}
		}

		// Check methods to invoke (if id is not _none)
		if(!t.id().equals("_none")) {
			// Look for methods
			for(Method m : handler.getClass().getDeclaredMethods()) {
				// Methods without annotations
				if(!m.isAnnotationPresent(FsmOnTransition.class))
					continue;

				// ID mismatch
				if(!m.getAnnotation(FsmOnTransition.class).value().equals(t.id())) {
					logger.info("Id mismatch for " + m);
					
					continue;
				}

				Class<?>[] methodParameterType = m.getParameterTypes();

				// First argument should be proxy
				//if(methodParameterType.length == 0 || methodParameterType[0] != proxy.getClass())
				//	throw new RuntimeException("Method " + m + " has FsmTransition annotation but first argument is not the input type " + proxy.getClass().getName());

				// Method parameters number mismatch
				if(parameterTypes.length != methodParameterType.length)
					throw new AssertionError("Method " + m + " mismatch parameters, invoked with " + parametersToString(parameterTypes) + " but method signature expectes " + parametersToString(methodParameterType));

				logger.info("Method " + m + " invoked with " + parametersToString(parameterTypes));

				if(logger.isDebugEnabled())
					logger.debug("Annotation " + t);

				// Invoke method
				return m.invoke(handler, args);
			}
		}

		return null;
	}

	private static String parametersToString(Class<?>[] parameterTypes) {
		StringBuilder sb = new StringBuilder("(");

		boolean first = true;

		for(Class<?> clazz : parameterTypes) {
			if(first)
				first = false;
			else
				sb.append(", ");

			sb.append(clazz.getSimpleName());
		}

		sb.append(")");

		return sb.toString();
	}

	public void handleLeavingState(Object proxy) throws Exception {
		// Look for methods
		for(Method m : handler.getClass().getDeclaredMethods()) {
			// Methods without annotations
			if(!m.isAnnotationPresent(FsmOnLeavingState.class))
				continue;

			String s = m.getAnnotation(FsmOnLeavingState.class).value();

			if(s.equals(handler.getCurrentState()) || s.equals("*"))
				m.invoke(handler, proxy);
		}
	}

	public void handleEnteringState(Object proxy) throws Exception {
		// Look for methods
		for(Method m : handler.getClass().getDeclaredMethods()) {
			// Methods without annotations
			if(!m.isAnnotationPresent(FsmOnEnteringState.class))
				continue;

			String s = m.getAnnotation(FsmOnEnteringState.class).value();

			if(s.equals(handler.getCurrentState()) || s.equals("*"))
				m.invoke(handler, proxy);
		}
	}

	public FsmProxyHandler(FsmHandler handler_, Object inputClass_) {
		handler = handler_;

		inputClass = inputClass_;
	}
}
